// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: xguo

#include "coin2/exchange/dydx_futures/api_order/native_private_client.h"

#include <memory>
#include <string>

#include "coin2/base/time.h"
#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/dydx_futures/api_util/rate_tracker.h"
#include "coin2/exchange/dydx_futures/symbology/product.h"

#define CHECK_API_LIMIT(action_type)                                                           \
  auto ts = GetCurrentTimestamp();                                                             \
  if (!HasEnoughRateLimit(ts, action_type)) {                                                  \
    LOG_EVERY_N(WARNING, 50) << "Hit rate limit or api temporary disabled until " << __func__; \
    api_block_info_[__func__]++;                                                               \
    return {};                                                                                 \
  }

using coin2::exchange::dydx_futures::symbology::DydxFuturesProduct;

auto DydxV3NativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  using DydxRateTracker = coin2::exchange::dydx_v3::api_util::DydxRateTracker;
  static_cast<DydxRateTracker*>(rate_tracker_.get())->SetNativeSymbol(order_spec.product);

  auto params = RestParams();
  auto product = DydxFuturesProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_price(formatter.FormatPrice(order_spec.price));
  params.set_size(formatter.FormatQty(order_spec.qty));
  params.set_client_order_id(std::to_string(proc_order_id));
  params.set_symbol(order_spec.product);
  order_amt_ = order_spec.price * order_spec.qty;

  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("BUY");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("SELL");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  params.set_type("LIMIT");
  if (order_spec.post_only) {
    params.set_post_only("true");  // it is ok to use string.
  } else {
    params.set_post_only("false");
  }

  params.set_time_in_force("GTT");
  if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
    params.set_time_in_force("FOK");
  } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
    params.set_time_in_force("IOC");
  }
  order_spec.order_expiration_ts = order_spec.order_created_time + static_cast<int64_t>(600 * 1e9);
  auto ts = Iso8601FromTimestamp(order_spec.order_expiration_ts);
  ts = ts.substr(0, 23) + "Z";
  params.set_expiration(ts);
  params.set_limit_fee("0.0015");

  CHECK_GT(position_id_, 0) << position_id_;
  auto order = StarkwareOrder(order_spec);
  order.pub_key = pub_key_;
  order.position_id = position_id_;
  params.set_signature(order.Sign(stark_key_));
  return params;
}

auto DydxV3NativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  const auto& order_spec = order_context.order_spec();
  using DydxRateTracker = coin2::exchange::dydx_v3::api_util::DydxRateTracker;
  static_cast<DydxRateTracker*>(rate_tracker_.get())->SetNativeSymbol(order_spec.product);

  DydxFuturesProduct product =
      DydxFuturesProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto params = RestParams();
  CHECK(order_context.has_exchange_order_id());
  params.set_exchange_order_id(order_context.exchange_order_id());
  return params;
}

DydxV3NativePrivateClient::DydxV3NativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config_override) {
  mea_ = mea;
  NativeClientConfig config;
  config.set_rest_host("api.dydx.exchange");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[DydxV3NativePrivateClient] config: " << config.DebugString();

  auths_.Construct<DydxRestAuth, NativeClientConfig>(config);
  using DydxRateTracker = coin2::exchange::dydx_v3::api_util::DydxRateTracker;
  rate_tracker_ = std::make_unique<DydxRateTracker>(mea_, config);

  stark_key_ = static_cast<DydxRestAuth*>(GetAuth())->GetPrivKey();
  pub_key_ = static_cast<DydxRestAuth*>(GetAuth())->GetPubKey();
}

std::shared_ptr<AsioHttpContext> DydxV3NativePrivateClient::QueryAccountInfo() {
  CHECK_API_LIMIT(ActionType::QUERY);
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/v3/accounts", &builder);

  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;
  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DydxV3NativePrivateClient::QueryOpenOrders() {
  CHECK_API_LIMIT(ActionType::QUERY);
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/v3/orders", &builder);

  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;
  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DydxV3NativePrivateClient::QueryFills() {
  CHECK_API_LIMIT(ActionType::QUERY);
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/v3/fills", &builder);
  api_req_info_[__func__]++;

  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;
  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DydxV3NativePrivateClient::PlaceOrder(const RestParams& params) {
  CHECK_API_LIMIT(ActionType::PLACE);
  UrlParamsBuilder builder;
  builder.PutPost("market", params.symbol());
  builder.PutPost("side", params.side());
  builder.PutPost("price", params.price());
  builder.PutPost("size", params.size());
  builder.PutPost("type", params.type());
  builder.PutPost("postOnly", params.post_only());
  builder.PutPost("clientId", params.client_order_id());
  builder.PutPost("expiration", params.expiration());
  builder.PutPost("timeInForce", params.time_in_force());
  builder.PutPost("limitFee", params.limit_fee());
  builder.PutPost("signature", params.signature());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/v3/orders", &builder);

  int consumed = std::ceil(40000.0 / order_amt_);
  consumed = std::min(std::max(consumed, 4), 100);
  Push(consumed, ts, ActionType::PLACE);
  api_req_info_[__func__]++;
  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DydxV3NativePrivateClient::CancelSingleOrder(
    const RestParams& params) {
  CHECK_API_LIMIT(ActionType::CANCEL);
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByDeleteWithSignature(
      "/v3/orders/" + params.exchange_order_id(),
      &builder);

  Push(1, ts, ActionType::CANCEL);
  api_req_info_[__func__]++;
  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DydxV3NativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

std::shared_ptr<AsioHttpContext> DydxV3NativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelSingleOrder(params);
}

std::shared_ptr<AsioHttpContext> DydxV3NativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelSingleOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> DydxV3NativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryAccountInfo();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      return QueryOpenOrders();
    }
    default: {
      return nullptr;
    }
  }
}

void DydxV3NativePrivateClient::Prepare() {
  using coin2::exchange::dydx_v3::api_util::DydxRateTracker;
  auto* tracker = dynamic_cast<DydxRateTracker*>(this->rate_tracker_.get());
  mb_->Subscribe("dydx_futures/rate_limit", [tracker](const std::string&, void* data) {
    std::string* rl = reinterpret_cast<std::string*>(data);
    LOG_FIRST_N(INFO, 20) << "dydx rate limit update!" << (*rl);
    tracker->UpdateRateLimitInfo(*rl);
  });
  mb_->Subscribe("dydx_futures/position_id", [this](const std::string&, void* data) {
    LOG_FIRST_N(INFO, 3) << "position id is set!";
    this->position_id_ = (uint64_t)(data);
  });
}
